/*
 * Copyright (c) 2023 fred
 * Licensed under the Apache License,Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import { Element } from '../nodes/Element';
import { Evaluator } from './Evaluator';
import HashMap from '@ohos.util.HashMap';
import { FirstFinder } from './Collector';

export abstract class StructuralEvaluator extends Evaluator {
  readonly evaluator: Evaluator

  constructor(evaluator: Evaluator) {
    super()
    this.evaluator = evaluator
  }
  // Memoize inner matches, to save repeated re-evaluations of parent, sibling etc.
  // root + element: Boolean matches. ThreadLocal in case the Evaluator is compiled then reused across multi threads
  readonly threadMemo: HashMap<Element, HashMap<Element, boolean>> = new HashMap()

  memoMatches(root: Element, element: Element): boolean {
    let rootMemo = this.threadMemo.get(root)
    if (rootMemo === undefined || rootMemo === null) {
      rootMemo = new HashMap<Element, boolean>()
      this.threadMemo.set(root, rootMemo)
    }
    let matches = rootMemo.get(element)
    if (matches === undefined || matches === null) {
      matches = this.evaluator.matches(root, element)
      rootMemo.set(element, matches)
    }
    return matches
  }

  reset() {
    this.threadMemo.clear()
    super.reset()
  }
}


export class EvaluatorRoot extends Evaluator {
  public matches(root: Element, element: Element): boolean {
    return root === element
  }

  cost(): number {
    return 1
  }

  public toString(): string {
    return ''
  }
}

export class StructuralHas extends StructuralEvaluator {
  readonly finder: FirstFinder

  constructor(evaluator: Evaluator) {
    super(evaluator)
    this.finder = new FirstFinder(evaluator)
  }

  public matches(root: Element, element: Element): boolean {
    // for :has, we only want to match children (or below), not the input element. And we want to minimize GCs
    for (let i = 0; i < element.childNodeSize(); i++) {
      let node = element.childNode(i)
      if (node instanceof Element) {
        let match = this.finder.find(element, node)
        if (match != null) {
          return true
        }
      }
    }
    return false
  }

  cost(): number {
    return 10 * this.evaluator.cost()
  }

  public toString(): string {
    return `:has(${this.evaluator.toString()})`
  }
}

export class StructuralNot extends StructuralEvaluator {

  constructor(evaluator: Evaluator) {
    super(evaluator)
  }

  public matches(root: Element, element: Element): boolean {
    return !this.memoMatches(root, element)
  }

  cost(): number {
    return 2 + this.evaluator.cost()
  }

  public toString(): string {
    return `:not(${this.evaluator.toString()})`
  }
}

export class StructuralParent extends StructuralEvaluator {

  constructor(evaluator: Evaluator) {
    super(evaluator)
  }

  public matches(root: Element, element: Element): boolean {
    if (root === element) {
      return false
    }
    let parent = element.parent()
    while (parent !== undefined && parent !== null) {
      if (this.memoMatches(root,parent)) {
        return true
      }
      if (parent === root) {
        break
      }
      parent = parent.parent()
    }
    return false
  }

  cost(): number {
    return 2 * this.evaluator.cost()
  }

  public toString(): string {
    return `${this.evaluator.toString()} `
  }
}

export class StructuralImmediateParent extends StructuralEvaluator {

  constructor(evaluator: Evaluator) {
    super(evaluator)
  }

  public matches(root: Element, element: Element): boolean {
    if (root === element) {
      return false
    }
    let parent = element.parent()
    return parent !== undefined && parent !== null && this.memoMatches(root,parent)
  }

  cost(): number {
    return 1 + this.evaluator.cost()
  }

  public toString(): string {
    return `${this.evaluator.toString()} > `
  }
}

export class StructuralPreviousSibling extends StructuralEvaluator {

  constructor(evaluator: Evaluator) {
    super(evaluator)
  }

  public matches(root: Element, element: Element): boolean {
    if (root === element) {
      return false
    }
    let sibling = element.firstElementSibling()
    while (sibling !== undefined && sibling !== null) {
      if (sibling === element) {
        break
      }
      if (this.memoMatches(root, sibling)) {
        return true
      }
      sibling = sibling.nextElementSibling()
    }
    return false
  }

  cost(): number {
    return 3 * this.evaluator.cost()
  }

  public toString(): string {
    return `${this.evaluator.toString()} ~ `
  }
}

export class StructuralImmediatePreviousSibling extends StructuralEvaluator {

  constructor(evaluator: Evaluator) {
    super(evaluator)
  }

  public matches(root: Element, element: Element): boolean {
    if (root === element) {
      return false
    }
    let prev = element.previousElementSibling()
    return prev !== undefined && prev !== null && this.memoMatches(root,prev)
  }

  cost(): number {
    return 2 + this.evaluator.cost()
  }

  public toString(): string {
    return `${this.evaluator.toString()} + `
  }
}